import { fromObject, generateMapFileComment } from 'convert-source-map'
import { readFileSync, writeFileSync } from 'fs'
import { writeFile } from 'fs/promises'
import { isObject, isString, once } from 'lodash-es'
import MagicString from 'magic-string'
import path, { posix as posixPath } from 'path'
import {
  commonInputFS,
  placeholderGlobalMatchRegexp,
  placeholderMatchRegexp,
  placeholderPrefix,
  placeholderRegexp,
  placeholderSuffix
} from '../common'
import { createLogger, ILogger } from '../logger'
import { nodeResolve } from '../resolve'
import {
  AcornCodeKey,
  AcornNodePri,
  Awaitable,
  CompluginBaseContext,
  CompluginContext,
  CompluginContextMeta,
  CompluginMinifyContext,
  CompluginOptions,
  CreateCompluginArgs,
  EmitAssetArgs,
  ExternalIdResult,
  SourceMap
} from '../types'
import {
  addSourceComment,
  base64url,
  createTempFileName,
  CWD,
  doHash,
  doThen,
  esm_default,
  getExtname,
  getInsertIndex,
  hasBuiltinsId,
  isAst,
  isEmitAssetArgs,
  iterableMap,
  loadDependency,
  makeRelativeLegalPosixPath,
  normalizeRawOptions,
  normalizeSourceMap,
  stringifyModuleId,
  toBuffer,
  toIdentifier,
  unBase64url,
  wrapTransformContext
} from '../utils'
import { createBundleProxy, getResolveUrl } from './utils'

type RollupModuleInfoPri = import('rollup').ModuleInfo & { _ast: AcornNodePri }

const priIsFirstCompluginKey = Symbol.for('complugin:rollup[0]')
const priPluginKey = Symbol.for('complugin:rollup[1]')
const priFsKey = Symbol.for('complugin:rollup[2]')
const priShimsKey = Symbol.for('complugin:rollup[3]')
const priIsNodeJsKey = Symbol.for('complugin:rollup[is-nodejs]')
const priTmpFileKey = Symbol.for('complugin:rollup[tmp]')
const priDataKey = Symbol.for('complugin:rollup[data]')

export default function getRollupPlugin<UserOptions = {}>(
  { name, enforce, factory }: CreateCompluginArgs<UserOptions>,
  options?: UserOptions
) {
  let version: string
  let watchMode = false
  let rawPlugin: CompluginOptions & { transformInclude: (id: string) => boolean }
  let meta: CompluginContextMeta
  let inputOptions: import('rollup').NormalizedInputOptions
  let outputOptions: import('rollup').NormalizedOutputOptions
  let tmpFileName: string
  let isFirstComplugin: boolean | undefined
  let baseResolve:
    | ((importee: string, importer?: string) => Promise<{ id: string; external: boolean } | undefined>)
    | undefined
  let pluginLogger: ILogger | undefined
  let hookLoggerMap: Record<string, ILogger> = Object.create(null)

  const emittedAssetContentWithSourceMap = new Map<string | 'ref', { code: string | Uint8Array; map?: SourceMap }>()
  const assetContentMap = new Map<
    string | 'ref',
    Awaitable<string | Uint8Array | { code: string | Uint8Array; map?: SourceMap }>
  >()
  const astMap = new Map<string | 'id', AcornNodePri>()
  const iifePrefix = '\0_v_iife_base64url:'
  const iifeRegexp = new RegExp(`^(?<=${iifePrefix})[\\w-]+$`)
  const emitFileMap = new Map<string | 'fileName', string | 'ref'>()
  const emitStaticFileMap = new Map<string | 'fileName', string | 'ref'>()
  const iifeMap = new Map<string | 'id', { ref: string; moduleId: string }>()
  let iifeBuildResults: {
    [moduleId: string]: {
      ref: string
      moduleId: string
      id: string
      result: ReturnType<typeof buildIife>
    }
  } = Object.create(null)

  const transformRef = (
    code: string,
    chunk: import('rollup').RenderedChunk,
    context: import('rollup').PluginContext,
    options: import('rollup').NormalizedOutputOptions
  ) => {
    const { format, sourcemap } = options
    const fixImports = (imports: string[]) => {
      imports.forEach((_import, i, array) => {
        const ref = _import.match(placeholderMatchRegexp)?.groups?.ref
        if (ref) {
          try {
            array[i] = context.getFileName(ref)
          } catch {}
        }
      })
    }

    fixImports(chunk.imports)
    fixImports(chunk.dynamicImports)

    const chunkFileName = chunk.fileName
    const magicString = new MagicString(code)
    let hasChange = false
    let base: string | undefined = (meta as any).base

    if (base) {
      if (base.includes(':')) {
        base = new URL('./', base).href
      } else if (base.startsWith('//')) {
        base = new URL('./', 'http:' + base).href.replace('http:', '')
      } else if (!base.endsWith('/')) {
        base = base + '/'
      }
    }

    const additionalStatementsArray: string[] = []
    const additionalStatementsSet = new Set<string>()
    const appendStatementsArray: string[] = []

    for (const match of code.matchAll(placeholderGlobalMatchRegexp)) {
      const start = match.index ?? 0
      const end = start + match[0].length

      const groups = match.groups
      if (groups) {
        const { k, ref } = groups

        let fileName: string

        try {
          fileName = context.getFileName(ref)
        } catch {
          continue
        }

        let replacement: string
        const relativePath = posixPath.relative(posixPath.dirname(chunkFileName), fileName)

        if (k) {
          replacement = `${stringifyModuleId(relativePath)}`
        } else if (base) {
          replacement = `(${JSON.stringify(base)} + ${JSON.stringify(fileName)})`
        } else {
          if (format === 'system') {
            replacement = getResolveUrl(`${JSON.stringify(relativePath)}, module.meta.url`)
          } else {
            const identifier = (replacement = toIdentifier(relativePath))
            additionalStatementsSet.add(
              `const ${identifier} = /* @__PURE__ */ ( __dirname + ${JSON.stringify('/' + relativePath)} )`
            )
          }
        }

        hasChange = true
        magicString.overwrite(start, end, replacement)
      }
    }

    let dirnameMatch
    if (
      format !== 'cjs' &&
      !(dirnameMatch = code.match(/(?:\b(var|let|const)\s+)?\b(__dirname|__filename)\b/))?.[1] &&
      (additionalStatementsSet.size > 0 || dirnameMatch)
    ) {
      hasChange = true

      const platform = (options as any)[priIsNodeJsKey] ? 'node' : plugin.$complugin.platform || 'browser'
      const fileUrlToPathIdentifier = toIdentifier('url.fileURLToPath')
      const pathDirnameIdentifier = toIdentifier('path.dirname')

      switch (format) {
        case 'es':
          if (platform === 'browser') {
            const shimsFileName = (options as any)[priShimsKey]?.() ?? 'complugin/dist/shims.mjs'
            const shimsFileId = './' + posixPath.relative(posixPath.dirname(chunkFileName), shimsFileName)
            additionalStatementsArray.push(
              `import { fileURLToPath as ${fileUrlToPathIdentifier}, dirname as ${pathDirnameIdentifier} } from ${JSON.stringify(
                shimsFileId
              )}`,
              `var __filename = /* @__PURE__ */ ${fileUrlToPathIdentifier}( import.meta.url )`,
              `var __dirname = /* @__PURE__ */ ${pathDirnameIdentifier}( __filename )`
            )
          } else {
            additionalStatementsArray.push(
              `import { fileURLToPath as ${fileUrlToPathIdentifier} } from 'url'`,
              `import { dirname as ${pathDirnameIdentifier} } from 'path'`,
              `var __filename = /* @__PURE__ */ ${fileUrlToPathIdentifier}( import.meta.url )`,
              `var __dirname = /* @__PURE__ */ ${pathDirnameIdentifier}( __filename )`
            )
          }

          break
        default:
          additionalStatementsArray.push(
            `var __filename, __dirname`,
            `if (typeof document === "object") {`,
            `  let __filename0 = document.currentScript?.src || document.baseURI`,
            `  if ( __filename0 ) {`,
            `    __filename = new URL(__filename0).pathname`,
            `    __dirname = new URL(".", __filename0).pathname`,
            `  }`,
            `}`
          )
          break
      }
    }

    if (hasChange) {
      const additionalCode = [...additionalStatementsArray, ...additionalStatementsSet].join('\n')
      const appendCode = appendStatementsArray.join(';\n')

      if (additionalCode) {
        magicString.appendLeft(getInsertIndex(code), '\n' + additionalCode + ';\n')
      }

      if (appendCode) {
        magicString.append('\n' + appendCode + ';\n')
      }

      return {
        code: magicString.toString(),
        map: sourcemap ? magicString.generateMap({ hires: true }) : undefined
      }
    }
  }

  const buildIife = (entryId: string, context: import('rollup').PluginContext) => {
    const rollup: typeof import('rollup') = loadDependency('rollup', true)
    // @ts-ignore
    const commonjs: typeof import('@rollup/plugin-commonjs').default = esm_default(
      loadDependency('@rollup/plugin-commonjs')
    )
    // @ts-ignore
    const json: typeof import('@rollup/plugin-json').default = esm_default(loadDependency('@rollup/plugin-json'))

    const returnValue = rollup.rollup({
      input: entryId,
      external: () => false,
      plugins: [
        {
          name: 'load',
          async resolveId(importee, importer, options) {
            const result: import('rollup').ResolvedId | undefined | null = await context.resolve(
              importee,
              importer,
              options
            )
            if (!result) return
            const { external, id } = result
            if (typeof id !== 'string') return
            if (external && /^[@a-z]([\w-]+)/i.test(result.id)) return

            return result
          },
          load(id) {
            const info = context.getModuleInfo(id)
            if (info) {
              const { code, ast } = info
              return { code: code || '', ast: ast ?? undefined }
            }
          },
          renderChunk(code, chunk, options) {
            return transformRef(code, chunk, context, options)
          }
        },
        commonjs?.(),
        json?.()
      ]
    })

    return returnValue
  }

  const renderIife = async (result: import('rollup').RollupBuild, fileName?: string) => {
    const rollupOutput = await result.generate({
      dir: '',
      inlineDynamicImports: true,
      format: 'iife',
      exports: 'none',
      entryFileNames: fileName || undefined
    })

    return rollupOutput.output[0]
  }

  const emitAsset = (asset: EmitAssetArgs, context: import('rollup').PluginContext) => {
    let { fileName, name, source = '' } = asset
    let ref: string | undefined
    if (fileName) {
      fileName = makeRelativeLegalPosixPath(fileName)
      ref = emitFileMap.get(fileName)
      if (ref) {
        assetContentMap.set(ref, source)
      }
    }
    if (!ref) {
      ref = context.emitFile({ name, fileName, type: 'asset' })
      assetContentMap.set(ref, source)
      if (fileName) {
        emitFileMap.set(fileName, ref)
      }
    }

    return placeholderPrefix + ref + placeholderSuffix
  }

  const createRawPlugin = once(() => {
    meta = plugin.$complugin.getMeta()
    return normalizeRawOptions(factory.call(meta, options, meta))
  })

  const rebuild = (id: string, context: import('rollup').PluginContext) => {
    writeFile(tmpFileName, '').catch()
  }

  let generateBundlePromise = Promise.resolve()

  const getLogger = () => pluginLogger ?? (pluginLogger = createLogger(`${name}:${plugin.$complugin.framework}`))
  const getHookLogger = (hook: string) =>
    hookLoggerMap[hook] ?? (hookLoggerMap[hook] = createLogger(`${name}:${plugin.$complugin.framework}/${hook}`))
  let transformIncludeThis: CompluginBaseContext
  let generateBundleThis: CompluginBaseContext

  const plugin: import('rollup').Plugin & {
    $complugin: {
      getMeta(): CompluginContextMeta
      getMinifyContext(hook: string, context: import('rollup').PluginContext): CompluginMinifyContext
      getContext(hook: string, id: string, context: import('rollup').PluginContext): CompluginContext
      resolveAssetId(
        asset: EmitAssetArgs,
        context: import('rollup').PluginContext
      ): Awaitable<undefined | null | ExternalIdResult>
      rebuild(id: string, context: import('rollup').PluginContext): void
      sourceMap: boolean
      framework: any
      platform?: 'browser' | 'node'
    }
    enforce?: 'pre' | 'post'
  } = {
    $complugin: {
      sourceMap: true,
      framework: 'rollup',
      getMeta() {
        let data: any
        let inputFS: any
        return {
          __proto__: null,
          logger: getLogger(),
          framework: plugin.$complugin.framework,
          version,
          watchMode,
          plugin,
          inputOptions,
          root: CWD,
          isProd: process.env.NODE_ENV === 'production',
          get inputFS() {
            return (
              inputFS ??
              (inputFS = (inputOptions as any)[priFsKey] ?? ((inputOptions as any)[priFsKey] = { ...commonInputFS }))
            )
          },
          get outputOptions() {
            return outputOptions
          },
          get data() {
            return (
              data ??
              (data = (inputOptions as any)[priDataKey] ?? ((inputOptions as any)[priDataKey] = Object.create(null)))
            )
          }
        }
      },
      getMinifyContext(hook: string, context) {
        return {
          logger: getHookLogger(hook),
          parse: context.parse.bind(context),
          addWatchFile: context.addWatchFile.bind(context),
          getWatchFiles: context.getWatchFiles.bind(context),
          emitFile: asset => {
            emitAsset(asset, context)
          },
          error: context.error.bind(context),
          warn: context.warn.bind(context),
          resolve: async (source, importer) => {
            const result = await context.resolve(source, importer, { skipSelf: true })
            if (result) {
              return {
                id: result.id,
                external: Boolean(result.external)
              }
            } else {
              return baseResolve?.(source, importer)
            }
          }
        }
      },
      getContext(hook: string, id, context) {
        return {
          ...this.getMinifyContext(hook, context),
          sourceMap: plugin.$complugin.sourceMap,
          rebuild: () => plugin.$complugin.rebuild(id, context),
          addWatchFile: fileName => context.addWatchFile(path.resolve(CWD, fileName)),
          emitStatic: fileName => {
            fileName = path.resolve(CWD, fileName)
            let ref = emitStaticFileMap.get(fileName)
            if (!ref) {
              const name = path.basename(fileName)
              emitStaticFileMap.set(fileName, (ref = context.emitFile({ name, type: 'asset' })))
            }

            return placeholderPrefix + ref + placeholderSuffix
          },
          emitAsset: asset => emitAsset(asset, context),
          emitChunk: async id => {
            const ref = context.emitFile({ id, type: 'chunk' })
            return placeholderPrefix + ref + placeholderSuffix
          },
          emitIife: async id => {
            let item = iifeMap.get(id)
            if (!item) {
              const modId = iifePrefix + base64url(id)
              iifeMap.set(
                id,
                (item = {
                  ref: context.emitFile({ id: modId, type: 'chunk', name: path.basename(id) }),
                  moduleId: modId
                })
              )
            }

            return placeholderPrefix + item.ref + placeholderSuffix
          }
        }
      },
      resolveAssetId(asset, context) {
        return {
          external: true,
          id: emitAsset(asset, context)
        }
      },
      rebuild
    },
    name,
    enforce,
    options() {
      version = this.meta.rollupVersion
      watchMode = this.meta.watchMode
    },
    buildStart(options) {
      if (watchMode && rebuild === plugin.$complugin.rebuild) {
        if (!(tmpFileName ?? (tmpFileName = (options as any)[priTmpFileKey]))) {
          tmpFileName = (options as any)[priTmpFileKey] = createTempFileName()
          try {
            writeFileSync(tmpFileName, '')
          } catch {}
          this.addWatchFile(tmpFileName)
        }
      }

      inputOptions = options
      rawPlugin = createRawPlugin()

      const rollupPlugins = options.plugins
      if (
        (rollupPlugins[rollupPlugins.length - 1] === plugin ||
          (rollupPlugins[rollupPlugins.length - 2] === plugin &&
            rollupPlugins[rollupPlugins.length - 1]?.name === 'stdin')) &&
        !rollupPlugins.some(plugin => plugin?.name === 'node-resolve')
      ) {
        const external = options.external
        baseResolve = async (importee, importer) => {
          if (external(importee, importer, false)) {
            return {
              id: importee,
              external: true
            }
          }

          return nodeResolve(importee, importer)
        }
      }

      if (isFirstComplugin === undefined) {
        if ((options as any)[priIsFirstCompluginKey]) {
          isFirstComplugin = false
        } else {
          isFirstComplugin = true
          ;(options as any)[priIsFirstCompluginKey] = true
        }
      }

      emittedAssetContentWithSourceMap.clear()

      return rawPlugin.buildStart?.call(plugin.$complugin.getMinifyContext('buildStart', this))
    },
    async buildEnd(err) {
      iifeBuildResults = Object.create(null)
      iifeMap.forEach(({ ref, moduleId }, id) => {
        iifeBuildResults[moduleId] = {
          moduleId,
          id,
          ref,
          result: buildIife(id, this)
        }
      })

      await rawPlugin.buildEnd?.call(plugin.$complugin.getMinifyContext('buildEnd', this))

      await Promise.all([
        ...iterableMap(emitStaticFileMap, async ([fileName, ref]) => {
          this.setAssetSource(ref, await meta.inputFS.readFile(fileName))
        }),
        ...iterableMap(assetContentMap, async ([ref, _source]) => {
          const source = await _source
          if (typeof source === 'string' || source instanceof Uint8Array) {
            this.setAssetSource(ref, source)
          } else if (isObject(source)) {
            emittedAssetContentWithSourceMap.set(ref, source)
          }
        })
      ])

      emitStaticFileMap.clear()
      emitFileMap.clear()
      iifeMap.clear()
    },
    augmentChunkHash({ facadeModuleId }) {
      if (!facadeModuleId) return
      const item = iifeBuildResults[facadeModuleId]
      if (!item) return
      return doHash(facadeModuleId + item.ref)
    },
    resolveId(importee, importer) {
      if (isFirstComplugin) {
        if (placeholderRegexp.test(importee)) {
          return { id: importee, external: true }
        } else if (iifeRegexp.test(importee)) {
          return importee
        }
      }

      return doThen(
        rawPlugin.resolveId?.call(plugin.$complugin.getMinifyContext('resolveId', this), importee, importer),
        result => {
          if (isEmitAssetArgs(result)) {
            return plugin.$complugin.resolveAssetId(result as EmitAssetArgs, this)
          } else if (result) {
            return result
          } else {
            return baseResolve?.(importee, importer)
          }
        }
      )
    },
    load(id) {
      if (isFirstComplugin) {
        let iifeImportModuleBase64url: string | undefined
        if ((iifeImportModuleBase64url = id.match(iifeRegexp)?.[0])) {
          return `import ${JSON.stringify(unBase64url(iifeImportModuleBase64url))};`
        }
      }

      return doThen(rawPlugin.load?.call(plugin.$complugin.getContext('load', id, this), id), result => {
        if (isString(result)) {
          return String(result)
        } else if (isObject(result)) {
          let { code, ast, copy, map, ...others } = result as any
          if (code != null) {
            code = String(code)

            if (copy) {
              let { name, fileName } = others

              return doThen(
                plugin.$complugin.resolveAssetId(
                  { source: map ? { code, map } : map, name: name || path.basename(id), fileName },
                  this
                ),
                copyResult => {
                  let copyId = copyResult?.id

                  if (copyId) {
                    const stringifyCopyId = JSON.stringify(copyId)
                    return `import _default from ${stringifyCopyId};export * from ${stringifyCopyId};export { _default as default };`
                  } else {
                    return `export {}`
                  }
                }
              )
            }

            if (isAst(ast)) {
              ;(ast as AcornNodePri)[AcornCodeKey] = code
              astMap.set(id, ast)
            }
          }

          return {
            code,
            ast,
            map,
            ...others
          }
        }
      })
    },
    transform(code, id) {
      const info = this.getModuleInfo(id) as RollupModuleInfoPri | null
      let ast: AcornNodePri | undefined

      if ((ast = astMap.get(id))) {
        astMap.delete(id)
        if (info && (ast as AcornNodePri)[AcornCodeKey] === code) {
          info._ast = info.ast = ast
        }
      }

      const { transform, transformInclude } = rawPlugin
      if (
        !transform ||
        !transformInclude.call(
          transformIncludeThis ?? (transformIncludeThis = { logger: getHookLogger('transformInclude') }),
          id
        )
      )
        return

      if (!ast && info) {
        ast = info._ast
      }

      return doThen(
        transform.call(
          wrapTransformContext(plugin.$complugin.getContext('transform', id, this), info?.isEntry === true),
          code,
          id,
          ast?.[AcornCodeKey] === code ? ast : undefined
        ),
        result => {
          if (info && result && !isString(result)) {
            const { code, ast } = result
            if (code != null && isAst(ast)) {
              ;(ast as AcornNodePri)[AcornCodeKey] = code
              info._ast = info.ast = ast
            }
          }
          return result
        }
      )
    },
    renderStart(this, outputOptions) {
      if (!rawPlugin) return
      ;(outputOptions as any)[priPluginKey] ??= plugin
      ;(outputOptions as any)[priShimsKey] ??= once(() => {
        const { entryFileNames } = outputOptions
        let ext = '.js'
        if (typeof entryFileNames === 'string') {
          ext = getExtname(entryFileNames)
        } else if (typeof entryFileNames === 'function') {
          ext = getExtname(
            entryFileNames({
              name: '_shims',
              type: 'chunk',
              modules: {},
              exports: [],
              facadeModuleId: null,
              isDynamicEntry: false,
              isEntry: false,
              isImplicitEntry: false
            })
          )
        }
        const fileName = '_complugin_shims' + ext
        try {
          this.emitFile({
            type: 'asset',
            fileName,
            source: readFileSync(path.join(__dirname, 'shims.mjs'))
          })
          return fileName
        } catch (error) {}
      })
      ;(outputOptions as any)[priIsNodeJsKey] ??= hasBuiltinsId(this.getModuleIds())

      const { sourcemap, sourcemapExcludeSources, dir, file } = outputOptions
      const outdir = path.resolve(dir ?? path.dirname(file ?? '0'))

      for (let [ref, { code, map }] of emittedAssetContentWithSourceMap) {
        const fileName = this.getFileName(ref)
        if (sourcemap) {
          map = isString(map) ? JSON.parse(map) : map
          if (map) {
            map = normalizeSourceMap(map, path.join(outdir, fileName), !sourcemapExcludeSources)
            code = isString(code) ? code : toBuffer(code).toString()

            switch (sourcemap) {
              case true:
                code = addSourceComment(
                  code,
                  generateMapFileComment(path.basename(fileName) + '.map', { multiline: true })
                )
              case 'hidden':
                this.emitFile({
                  type: 'asset',
                  source: JSON.stringify(map),
                  fileName: fileName + '.map'
                })
                break
              case 'inline':
                code = addSourceComment(code, fromObject(map).toComment({ multiline: true }))
                break
            }
          }
        }

        this.setAssetSource(ref, code)
      }
    },
    renderChunk(code, chunk, outputOptions) {
      if (!rawPlugin) return

      if ((outputOptions as any)[priPluginKey] === plugin) {
        return transformRef(code, chunk, this, outputOptions)
      }
    },
    generateBundle(output_options, bundle, isWrite) {
      if (rawPlugin) {
        return (generateBundlePromise = generateBundlePromise.catch().then(async () => {
          outputOptions = output_options

          const iifeBuildResultValues = Object.values(iifeBuildResults)

          if (iifeBuildResultValues.length > 0) {
            const shouldRemoveModules = new Set<string>()
            await Promise.all(
              iifeBuildResultValues.map(async ({ ref, result }) => {
                const fileName = this.getFileName(ref)
                const chunk = bundle[fileName]

                if (chunk?.type === 'chunk') {
                  const renderResult = await renderIife(await result, fileName)

                  chunk.code = renderResult.code
                  chunk.map = renderResult.map

                  for (const _import of [...chunk.dynamicImports, ...chunk.imports]) {
                    const _importChunk = bundle[_import]
                    if (_importChunk?.type === 'chunk' && !_importChunk.isEntry) {
                      shouldRemoveModules.add(_import)
                    }
                  }

                  chunk.dynamicImports = []
                  chunk.imports = []
                  chunk.exports = []
                  chunk.modules = renderResult.modules
                  chunk.referencedFiles = renderResult.referencedFiles
                }
              })
            )

            for (const chunk of Object.values(bundle)) {
              if (chunk?.type === 'chunk') {
                for (const id of chunk.dynamicImports) {
                  shouldRemoveModules.delete(id)
                }
                for (const id of chunk.imports) {
                  shouldRemoveModules.delete(id)
                }
              }
            }

            for (const id of shouldRemoveModules) {
              delete bundle[id]
            }
          }

          await rawPlugin?.generateBundle?.call(
            generateBundleThis ?? (generateBundleThis = { logger: getHookLogger('generateBundle') }),
            createBundleProxy(bundle),
            bundle
          )
        }))
      } else {
        const bundleProxy = createBundleProxy(bundle)
        const meta: CompluginContextMeta = {
          logger: getLogger(),
          framework: 'rollup',
          version: this.meta.rollupVersion,
          watchMode: this.meta.watchMode,
          plugin: Object.create(null),
          inputOptions: Object.create(null),
          outputOptions: output_options,
          inputFS: (output_options as any)[priFsKey] ?? ((output_options as any)[priFsKey] = { ...commonInputFS }),
          data: (output_options as any)[priDataKey] ?? ((output_options as any)[priDataKey] = Object.create(null)),
          root: CWD,
          isProd: process.env.NODE_ENV === 'production'
        }
        return factory
          .call(meta, options, meta)
          .generateBundle?.call(
            generateBundleThis ?? (generateBundleThis = { logger: getHookLogger('generateBundle') }),
            bundleProxy,
            bundle
          )
      }
    }
  }

  return plugin
}
